[TSCTF-J2023]逆向工程题解

前言

TSCTF-J2023 Reverse试题落实立德树人根本任务,遵循德智体美劳全面发展要求,贯彻《深化新时代CTF改革总体方案》,体现了TSCTF-J改革的方向。试卷突出逆向工程学科特点,加强基础性与关键能力考查,充分发挥逆向工程学科的选拔与引导功能。TSCTF-J2023 Reverse坚持立德树人,体现CTF文化的育人价值,突出理性思维的价值,注重计算机基础性,引导学生对CTF概念、方法更深刻的认知,在基础性、综合性、应用性、创新性等方面都进行了深入的考查。试题稳中有变,变中有新,难度设计科学。 较好地发挥了TSCTF-J的选拔功能,对萌新学习逆向知识发挥了积极的引导和促进作用。
题目在此

The_Path

不知道各位在做这道题的时候有什么想法,反正我是挺想骂街的。添加了与[2019红帽杯]easyRE一样的主动防御。本着不能只有我一个人受苦的优良品德,出了这道题。打开看见:

给人感官极其良好,但是稍微往左瞥一眼就会看见:

是的,我创造了50000个函数。根据提示摁下shift+F12可以根据特殊字符串找到:

然而,解完却得到了https://bbs.kanxue.com/thread-254172.htm。 非常好,被骗了。观察数据可以发现在key和ans上面存在着enc和box两个没用到但已初始化的数组:

跟进可以找到真正的加密函数:

解得flag:TSCTF-J{Welcome_to_TSCTF-J_reverser!}
将文件用010editor打开可以在最后看见彩蛋:

The_Encryption

这道题很繁琐,很傻逼。打开看见:

根据他们在栈上的位置可以改改名字:

点进去可以看出encrypt1是变表的base64加密,encrypt2是TEA,encrypt3是RC4。用我们小学二年级就会的代码可以写出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include <iostream>
#include <cstring>

using namespace std;

char base64char[100] = "AKLMNOPQRbGH34ScdefghIJvwxyz012ijklmnopqVWXYZa56789+/rstuBCDEFTU";

int __cdecl base64_decode(char *base64, char *originChar)
{
int v2; // eax
int v3; // eax
int v4; // eax
unsigned __int8 temp[4]; // [rsp+23h] [rbp-Dh] BYREF
unsigned __int8 k; // [rsp+27h] [rbp-9h]
int j; // [rsp+28h] [rbp-8h]
int i; // [rsp+2Ch] [rbp-4h]

i = 0;
j = 0;
while ( base64[i] )
{
memset(temp, 255, sizeof(temp));
for ( k = 0; k <= 0x3Fu; ++k )
{
if ( base64char[k] == base64[i] )
temp[0] = k;
}
for ( k = 0; k <= 0x3Fu; ++k )
{
if ( base64char[k] == base64[i + 1] )
temp[1] = k;
}
for ( k = 0; k <= 0x3Fu; ++k )
{
if ( base64char[k] == base64[i + 2] )
temp[2] = k;
}
for ( k = 0; k <= 0x3Fu; ++k )
{
if ( base64char[k] == base64[i + 3] )
temp[3] = k;
}
v2 = j++;
originChar[v2] = (temp[1] >> 4) & 3 | (4 * temp[0]);
if ( base64[i + 2] == 61 )
break;
v3 = j++;
originChar[v3] = (temp[2] >> 2) & 0xF | (16 * temp[1]);
if ( base64[i + 3] == 61 )
break;
v4 = j++;
originChar[v4] = temp[3] & 0x3F | (temp[2] << 6);
i += 4;
}
return j;
}

void TEA_decrypt(char * s2)
{
unsigned int sum = 0, v0 = 0, v1 = 0;
unsigned int delta = 0x61C88647, k[5] = {1029, 3847, 5674, 8392};
int i;
for(i = 0; i < 4; i++)
{
v0 = v0 * 256 + (s2[i] & 0xff);//注意这里,这个地方很玄学。如果你在这个for之前将s2[]输出出来,你会发现前几位都是负数。
v1 = v1 * 256 + (s2[i+4] & 0xff);//不加这个变化历程会是ffffffab -> ffffaaa7 -> ffaaa722 -> aaa7219b,有很明显的错误。
}
for(i = 0; i < 32; i++)
{
sum -= delta;
}
for(i = 0; i < 32; i++)
{
v1 -= (v0 + sum) ^ (16 * v0 + k[2]) ^ ((v0 >> 5) + k[3]);
v0 -= (v1 + sum) ^ (16 * v1 + k[0]) ^ ((v1 >> 5) + k[1]);
sum += delta;
}
for(i = 3; i >= 0; i--)
{
s2[i] = v0 % 256;
s2[i+4] = v1 % 256;
v0 /= 256;
v1 /= 256;
}
for(i = 0; i < 8; i++)
{
printf("%c", s2[i]);
}
cout << '-';
}

void rc4_init(unsigned char *s, unsigned char *key, unsigned long Len)
{
int i = 0, j = 0;
char k[256] = {0};
unsigned char tmp = 0;
for (i = 0; i<256; i++)
{
s[i] = i;
k[i] = key[i%Len];
}
for (i = 0; i<256; i++)
{
j = (j + s[i] + k[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}

void rc4_crypt(unsigned char *s, unsigned char *Data, unsigned long Len)
{
int i = 0, j = 0, t = 0;
unsigned long k = 0;
unsigned char tmp;
for (k = 0; k<Len; k++)
{
i = (i + 1) % 256;
j = (j + s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
t = (s[i] + s[j]) % 256;
Data[k] ^= s[t];
}
}

int main()
{
int randint[99] = {0x19, 0x78, 0x38, 0xa, 0xc7, 0x77, 0x89, 0x41, 0xdc, 0xf4, 0xa1, 0xa0, 0x21, 0xfe, 0x29, 0x79, 0x6, 0x63, 0x5f, 0x1c, 0x2f, 0x14, 0x4f, 0xd2, 0xb2, 0x83, 0xa2, 0xaa};
int dis[99] = {-94,46,-54,-107,150,10,85,-32,169,163,75,99,118,343,7,222,-87,204,162,133,13,-47,135,318,235,243,147,176};
char ans[99], s1[15], s2[15], s3[15], de64[15] = {};
int i;
for(i = 0; i < 28; i++)
{
ans[i] = randint[i] - dis[i];
}
for(i = 0; i < 12; i++)
{
s1[i] = ans[i];
}
for(i = 12; i < 20; i++)
{
s2[i-12] = ans[i];
}
for(i = 20; i < 28; i++)
{
s3[i-20] = ans[i];
}
base64_decode(s1, de64);
cout << de64 << '-';
TEA_decrypt(s2);
unsigned char s[256] = {};
char key[99] = "It's_almost_done";
rc4_init(s, (unsigned char *)key, strlen(key));
rc4_crypt(s, (unsigned char*)s3, 8);
for(i = 0; i < 8; i++)
{
cout << s3[i];
}
return 0;
}

得到flag:TSCTF-J{ai8v3m0z-2kf6cj1a-74bfuzx6}
想不到吧,flag是乱码,哈哈哈哈哈哈。

The_GoBang

C#逆向,逆向软件使用dnSpy。不推荐运行附件,攻击性挺强的。
在GoBang命名空间,Flag类中找到GetFlag方法:

这个算法乍一看好像并不可逆,但仔细观察可以发现如果执行if块,得到的必定是偶数,如果执行else块,得到的必定是奇数。
据此可以写出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

using namespace std;

int main()
{
long long s = 0x1C888E368C54BA0F;
int i;
for(i = 0; i < 32; i++)
{
if(s & 1 == 1)
{
s = (s ^ -5557806387376563891);
s >>= 1;
s |= 0x8000000000000000;//强转为负数
}
else
{
s = s >> 1;
s &= 0x7fffffffffffffff;//强转为正数
}
}
cout << hex << s;
return 0;
}

据此可以得到7677464758678594。
根据GoBang命名空间,Form1类panel2_MouseDown方法可以知道这串数其实是下棋的点位。
运行得到flag:TSCTF-J{F760CE18031A7492160EB3C6742C68A1}
不想运行直接MD5也是可以的。

The_Step

此题考查选手动态调试的能力。打开之后改改名字看见:

main函数戛然而止必定不对劲,选择动态调试。发现进入:

接着进行可以发现进入:

查看off_4C4018可以发现它其实是array[10]。
接着进行可以发现进入:

此时,要么硬读汇编,要么全选之后摁P,再摁F5,就能看见伪代码。
接着进行可以发现进入:

执行完4C110B的代码后将鼠标放在edx上可以发现edx其实就是array。
接着进行可以发现进入:

这里就是对比数据了。至此,我们可以写出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
int s[40] = {4188, 4178, 4139, 4191, 4166, 3872, 3996, 4088, 3893, 3813, 3947, 4634, 4485, 4508, 4319, 4212, 3311, 3311, 3027, 2909, 3503, 3312, 3290, 5207, 5055, 5642, 5606, 5094, 4118, 3782, 4401, 4322, 119};
int i;
for(i = 0; i < 33; i++)
{
s[i] += i * i;
s[i] /= 3;
}
for(i = 10; i < 27; i++)
{
s[i] -= 0x34;
}
for(i = 0; i < 33; i++)
{
s[i] ^= i * (i + 1);
s[i] ^= 0x520;
printf("%c", s[i]);
}
return 0;
}

运行得到flag:TSCTF-J{Oh_You_CAn_ReAlLY_dAnCe!}

The_Magic

TLS回调函数+SEH异常处理。
跟进线程会解出TSCTF-J{You_are_too_young_too_naive!}。同时在main和thread中下断点均断不住程序。根据提示找到TLS回调函数:

第一段是进行33位的输入。找到第二段:

可以找到加密部分。但是如此解密会得到一串乱码。如果你尝试过动调,会发现程序在不停地报错,但是如果选择将错误反馈给程序又能继续执行。根据提示查看汇编,找到try-except块:

由于except块不在正常的程序执行流程当中,因此IDA会在分析伪代码的时候将其忽略。接下来可以硬读汇编或者让某一个跳转语句跳转到except块,然后再F5看伪代码。总之,可以写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <cstring>

using namespace std;

int Seed = 150;

int getrand()
{
Seed = (Seed + 1029) * 3847 % 5665;
return Seed;
}

int main()
{
int enc[100] = { 'T', 'S', 'C', 'T', 'F', '-', 'J', '{', 111, 186, 117, 71, 44, 54, 93, 43, 179, 68, 158, 25, 145, 107, 229, 146, 220, 128, 220, 203, 195, 73, 179, 12, 7, '}' };
int Xor[100] = { 611, 613, 709, 611, 591, 589, 599, 597, 4732, 2686, 1877, 2929, 2678, 7275, 5979, 3657, 3879, 8962, 7445, 2694, 3384, 3710, 4938, 2832, 2329, 4215, 3152, 4911, 6525, 3445, 2071, 5765, 3700, 3156, 4178, 7234, 3437, 2437, 5236, 7265, 5391, 6257, 5756, 3426, 3446, 7432, 3871, 3442, 2200, 589 };
int i, j;
printf("TSCTF-J{");
for(i = 8; i <= 32; i++)
{
int randint = getrand();
for(j = 32; j < 126; j++)
{
int input = j;
input = (unsigned char)((input + 520) ^ (randint + 996));
if(input >> 7 == 0)
{
input = (unsigned char)((input + 123) ^ (Xor[i] - 456));
}
if(input == enc[i])
{
cout << char(j);
}
}
}
printf("}");
return 0;
}

运行得出flag:TSCTF-J{Y0u_4r3_a_8r3at_m4g1cIan!}

The_Shell

手脱UPX壳。根据提示的这篇文章使用x64dbg手动脱壳。打开可以看见程序开头的一堆push已经被nop掉了,但我们仍然可以根据RSP的变化下断点运行,寻找大跳。

最后找到程序的OEP是0x4014E0。

修复转储后用IDA打开可以看见:

这个非常简单,可以写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

int add[50] = {4, 49, 40, 60, 200, 187, 77, 199, 117, 10, 157, 92, 104, 87, 132, 134, 147, 32, 145, 99, 42, 18, 208, 167, 159, 44, 84, 227, 209, 34, 19, 8, 129, 255};
int ans[50] = {88, 132, 107, 144, 270, 232, 151, 322, 200, 127, 256, 196, 199, 184, 227, 232, 248, 129, 262, 215, 147, 120, 325, 275, 254, 127, 156, 296, 285, 110, 52, 41, 162, 380};

int main()
{
int i;
for(i = 0; i < 34; i++)
{
printf("%c", ans[i] - add[i]);
}
return 0;
}

解得flag:TSCTF-J{Such_a_beautiful_SHELL!!!}
在查找数据时可以找到彩蛋:

The_Art

代码自修改+花指令。
打开后发现无法F5,且汇编指令只能看懂这些:

硬读汇编可知程序将接下来的代码段每一位异或了0x77。写出如下IDC脚本:

1
2
3
4
5
6
7
8
9
10
#include <idc.idc>

static main()
{
auto i;
for(i = 0x40139c; i < 0x40171f; i++)
{
PatchByte(i, Byte(i) ^ 0x77);
}
}

然而,依旧无法F5,查看汇编可以找到:

将E9改为90后终于可以F5了,可以看到:

很明显,这段代码并不完整,继续查看汇编可以找到:

将C3改为90后就可以得到最终的伪代码:

通过findcrypt插件可以知道程序使用的是SM4加密,我们的输入是秘钥的一部分。需要使得加密后的数据是v15。考虑爆破,写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <stdio.h>
#define u8 unsigned char
#define u32 unsigned long

const u8 Sbox[256] = {
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
};

const u32 FK[4] = {
0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc
};

const u32 CK[32] = {
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
};

u32 functionB(u32 b) {
u8 a[4];
short i;
a[0] = b / 0x1000000;
a[1] = b / 0x10000;
a[2] = b / 0x100;
a[3] = b;
b = Sbox[a[0]] * 0x1000000 + Sbox[a[1]] * 0x10000 + Sbox[a[2]] * 0x100 + Sbox[a[3]];
return b;
}

u32 loopLeft(u32 a, short length) {
short i;
for(i = 0; i < length; i++) {
a = a * 2 + a / 0x80000000;
}
return a;
}

u32 functionL1(u32 a) {
return a ^ loopLeft(a, 2) ^ loopLeft(a, 10) ^ loopLeft(a, 18) ^ loopLeft(a, 24);
}

u32 functionL2(u32 a) {
return a ^ loopLeft(a, 13) ^ loopLeft(a, 23);
}

u32 functionT(u32 a, short mode) {
return mode == 1 ? functionL1(functionB(a)) : functionL2(functionB(a));
}

void getRK(u32 MK[], u32 K[], u32 RK[]) {
int i;
for(i = 0; i < 4; i++) {
K[i] = MK[i] ^ FK[i];
}
for(i = 0; i < 32; i++) {
K[(i+4)%4] = K[i%4] ^ functionT(K[(i+1)%4] ^ K[(i+2)%4] ^ K[(i+3)%4] ^ CK[i], 2);
RK[i] = K[(i+4)%4];
}
}

void iterate32(u32 X[], u32 RK[]) {
short i;
for(i = 0; i < 32; i++) {
X[(i+4)%4] = X[i%4] ^ functionT(X[(i+1)%4] ^ X[(i+2)%4] ^ X[(i+3)%4] ^ RK[i], 1);
}
}

void reverse(u32 X[], u32 Y[]) {
short i;
for(i = 0; i < 4; i++){
Y[i] = X[4 - 1 - i];
}
}

void encryptSM4(u32 X[], u32 RK[], u32 Y[]) {
iterate32(X, RK);
reverse(X, Y);
}

void decryptSM4(u32 X[], u32 RK[], u32 Y[]) {
short i;
u32 reverseRK[32];
for(i = 0; i < 32; i++) {
reverseRK[i] = RK[32-1-i];
}
iterate32(X, reverseRK);
reverse(X, Y);
}

int main(void) {
u32 X[4];
u32 MK[4];
u32 RK[32];
u32 K[4];
u32 Y[4];
short i;
int a, b, c;
for(a = 0; a < 255; a++)
{
printf("%d\n", a);
for(b = 0; b < 255; b++)
{
for(c = 0; c < 255; c++)
{
MK[0] = 0xa9873700 + a;
MK[1] = 0xf75b8a00 + b;
MK[2] = 0xfe911800 + c;
MK[3] = 0x705ade09;
X[0] = 0xCACEE1CC;
X[1] = 0xD5D2BAA3;
X[2] = 0xCDBEF5CA;
X[3] = 0xBFA3C7CA;
getRK(MK, K, RK);
encryptSM4(X, RK, Y);
if(Y[0] == 0x97d579f4 && Y[1] == 0xdfa9f2c7 && Y[2] == 0x5d2daae7 && Y[3] == 0x204bffbb)
{
printf("正确秘钥:%d %d %d\n", a, b, c);
return 0;
}
}
}
}
return 0;
}

4分钟之后可以解得正确秘钥:241 129 204。
得到flag:TSCTF-J{99B099BA10008E22180634253E984A88}

The_Native

本文采用JEB作为反汇编软件。
打开后可以在Manifest中找到程序的入口点是com.linjhs.ezandroid.MainActivity:

它调用了两个本地的库,一个叫MyString(),一个是Cal()。将libezandroid.so提取出来并用IDA打开后就可以找到他们。
对于MyString(),它只是将数据提取了出来然后返回。

对于Cal(),有用的部分只有异或0xC。

在MainActivity中可以找到异或52。因此可以写出解题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string.h>

using namespace std;

int main()
{
char v[] = {108, 107, 123, 108, 126, 21, 114, 67, 108, 80, 93, 103, 86, 89, 76, 81, 110, 93, 103, 89, 84, 79, 89, 65, 13, 103, 80, 11, 9, 72, 103, 65, 8, 77, 25, 69};
for (int i = 0; i < strlen(v); i++)
v[i] ^= 52;
for (int i = 0; i < strlen(v); i++)
v[i] ^= 12;
cout << v;
return 0;
}

运行得出flag:TSCTF-J{The_natiVe_alway5_h31p_y0u!}

The_Devil

虚拟机逆向。感受脑干在未知的地方被碾得支离破碎的感觉吧!哈哈哈哈哈哈哈!
经过分析可以得出:

VirtualMachineInit()将寄存器及zf指示位置零。VirtualMachine1()分析后如下:

其中reg[0]是eip,reg[1]~reg[4]是eax,ebx,ecx,edx,reg[5]是zf指示位。
可以看出case1X是mov指令、case2X是add指令、case3X是sub指令、case4X是xor指令、case5X是imul指令、case6X是自创模运算modul指令、case7X是and指令、case8X是or指令、case90是not指令、case100是cmp指令、case110是jne指令、case111是je指令、case255是retn指令。再根据opcode可以复现出汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mov   eax, 0                15,1,0,
Label1:
mov ebx, input[eax] 12,2,1,
sub ebx, eax 30,2,1,
imul ebx, 4 55,2,4,
mov intarray[eax], ebx 13,1,2,
add eax, 1 25,1,1,
mov ebx, 64 15,2,64,
cmp eax, ebx 100,1,2,
jne Label1 110,3,

mov eax, 0 15,1,0,
Label2:
mov ebx, eax 10,2,1,
add ebx, 8 25,2,8,
modul ebx, 64 65,2,64,
mov ecx, intarray[eax] 14,3,1,
not ecx 90,3,
and ecx, intarray[ebx] 74,3,2,
mov edx, intarray[ebx] 14,4,2,
not edx 90,4,
and edx, intarray[eax] 74,4,1,
or ecx, edx 80,3,4,
mov intarray[eax], ecx 13,1,3,
add eax, 1 25,1,1,
mov ecx, eax 10,3,1,
xor ecx, 64 45,3,64,
mov edx, 0 15,4,0,
cmp ecx, edx 100,3,4,
jne Label2 110,29,
retn 255

把上面的主要部分翻译成人话就是

1
2
3
input[i] -= i;
input[i] *= 4;
input[i] ^= input[(i + 8) % 64];

VirtualMachine2()可分析如下:

这一部分的加密由reg作为索引,进行或非操作。而此部分的opcode是加密过的,每次用完又会加密回去。加密所用的key明面上是"Clovershrub",动调可以发现实际上是"ClovershrubHMS"。可以考虑将所有的opcode自行解密,本文采用设置条件断点,让IDA输出的方法。

IDAPython脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
reg_addr = 0x00007FF65F3097D0
rsp = idc.get_reg_value('rsp')
p0 = ida_bytes.get_dword(rsp + 0x28)
p1 = ida_bytes.get_dword(rsp + 0x24)
p2 = ida_bytes.get_dword(rsp + 0x20)

if p1 & 0x80000000 != 0:
p1 = -(0xffffffff - p1 + 1)
if p2 & 0x80000000 != 0:
p2 = -(0xffffffff - p2 + 1)

reg_p1 = ida_bytes.get_dword(reg_addr + p1 * 4)
reg_p2 = ida_bytes.get_dword(reg_addr + p2 * 4)

if reg_p1 & 0x80000000 != 0:
reg_p1 = -(0xffffffff - reg_p1 + 1)
if reg_p2 & 0x80000000 != 0:
reg_p2 = -(0xffffffff - reg_p2 + 1)

print("reg[%d] = ~(reg[%d] | reg[%d]) "%(p0, p1, p2), "reg[%d] = ~( %d | %d )"%(p0, reg_p1, reg_p2))

在输出窗口可以看见他吐出来了一大堆东西。可对其进行分组,这是其中的第一个:

1
2
3
4
5
6
7
8
9
10
reg[1] = ~(reg[8] | reg[8])        reg[1] = ~( 0 | 0 )
reg[2] = ~(reg[-432] | reg[-432]) reg[2] = ~( 76 | 76 )
reg[2] = ~(reg[2] | reg[2]) reg[2] = ~( -77 | -77 )
reg[3] = ~(reg[2] | reg[1]) reg[3] = ~( 76 | -1 )
reg[1] = ~(reg[8] | reg[8]) reg[1] = ~( 0 | 0 )
reg[1] = ~(reg[1] | reg[1]) reg[1] = ~( -1 | -1 )
reg[2] = ~(reg[-432] | reg[-432]) reg[2] = ~( 76 | 76 )
reg[4] = ~(reg[2] | reg[1]) reg[4] = ~( -77 | 0 )
reg[1] = ~(reg[4] | reg[3]) reg[1] = ~( 76 | 0 )
reg[8] = ~(reg[1] | reg[1]) reg[8] = ~( -77 | -77 )

通过观察可以找到reg[8]就是intarray[0],reg[-432]是另一个数组——text[]的首位元素。进IDA查找可以知道text[]=“LoveCanConquerEverything”。分析或非操作可知其实就是在进行异或操作。我们可以写出解题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
int enc[100] = {88, 191, 222, 113, 215, 353, 42, 195, 127, 126, 89, 105, 125, 142, 73, 174, 45, 214, 193, 88, 188, 21, 14, 219, 68, 79, 446, 65, 3, 65, 434, 387, 399, 86, 121, 53, 441, 90, 77, 406, 113, 422, 117, 448, 420, 397, 114, 459, 24, 23, 46, 77, 439, 117, -238, 3, 207, 38, 113, 249, 5, 426, -115, 14};
char text[50] = "LoveCanConquerEverything";
int i;
for(i = 0; i < 64; i++)
{
enc[i] ^= text[i % 24];
}
for(i = 63; i >= 0; i--)
{
enc[i] ^= enc[(i + 8) % 64];
}
for(i = 0; i < 64; i++)
{
enc[i] /= 4;
enc[i] += i;
}
for(i = 0; i < 64; i++)
{
printf("%c", enc[i]);
}
return 0;
}

运行得出flag:TSCTF-J{You_successfully_dispelled_the_devil_within_three_days!}

后记

这次经历还是收获了很多的。在赛前去各个网站扒往年的题学习它们的设计和解题的方法、赛中给选手回答问题都让我受益匪浅。要说遗憾的话可能是今年的题出得确实有点难,没见到几个成功屠龙的猛士。在看大家写得题解的时候发现有的师傅放弃一道题的原因竟然是害怕接下来还会有坑。哎,为什么要把我想象得如此邪恶。呜呜呜呜呜( ಥ _ ಥ )

不过你们应该还是挺喜欢我出的题的,对吧对吧?( ≧ ∇ ≦ ) /